# pylint: disable=no-member
import configparser
import sys
import os
import numpy as np
import cv2
import xlrd
import time
import datetime
import tkinter as tk
from tkinter import font as tf
from tkinter import filedialog
from PIL import Image, ImageTk

class CameraControl():
    def __init__(self, camera_id: int = 0):

        ini_file = False
        config = configparser.ConfigParser()
        if os.path.exists('./data/config.ini'):
            config.read('./data/config.ini', encoding='utf-8')
            ini_file = True

        # 哪一个摄像头
        self.camera_id = camera_id
        if ini_file is True and config.has_option('photo', 'camera_id'):
            self.camera_id = config.getint('photo', 'camera_id')

        # 头像占比大小
        head_min_width = 0.15
        if ini_file is True and config.has_option('photo', 'head_min_width'):
            head_min_width = config.getfloat('photo', 'head_min_width')

        head_max_width = 0.4
        if ini_file is True and config.has_option('photo', 'head_max_width'):
            head_max_width = config.getfloat('photo', 'head_max_width')

        self.xml = './data/haarcascade_frontalface_default.xml'
        if ini_file is True and config.has_option('photo', 'xml'):
            self.xml = config.get('photo', 'xml').strip('\"')

        # 脸中心在相片中距底边的高度点总高度的比例
        self.face_center_scale = 0.56
        if ini_file is True and config.has_option('photo', 'face_center_scale'):
            self.face_center_scale = config.getfloat(
                'photo', 'face_center_scale')
        # 相片高宽比
        self.h_w = 1.4
        if ini_file is True and config.has_option('photo', 'h_w'):
            self.h_w = config.getfloat('photo', 'h_w')

        # 相片高是脸高的多少倍
        self.scale_h = 1.8
        if ini_file is True and config.has_option('photo', 'scale_h'):
            self.scale_h = config.getfloat('photo', 'scale_h')

        # 最后统一尺寸时,相片宽度
        self.final_photo_width = 600
        if ini_file is True and config.has_option('photo', 'final_photo_width'):
            self.final_photo_width = config.getint(
                'photo', 'final_photo_width')

        # 画人脸线框的颜色
        self.color_err = (255, 0, 0, 255)
        self.color_ok = (0, 255, 0, 255)
        self.color = (0, 255, 0, 255)
        # 照片识别成功,可以保存的标志
        self.photo_ready = False
        # 识别成功的照片,对应的坐标
        self.x0 = self.y0 = self.x1 = self.y1 = 0
        # 预览窗口的宽和高
        self.preview_width = 640
        self.preview_height = 480
        # 人脸别模型
        self.face_model = cv2.CascadeClassifier(self.xml)
        # 相机操作用到的对象
        self.cap = cv2.VideoCapture()
        # 打开摄像头
        self.cap.open(self.camera_id)
        # 查看摄像头是否打开
        if self.cap.isOpened() is False:
            print("摄像头打不开")
            sys.exit(0)
        # MJPG解码器
        fourcc = cv2.VideoWriter.fourcc('M', 'J', 'P', 'G')
        # 设置很大的宽高,再读取,从而确定摄像头支持的最大宽高
        self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
        self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
        self.cap.set(cv2.CAP_PROP_FPS, 30)
        self.photo_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
        self.photo_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
        # 设置传输模式
        self.cap.set(cv2.CAP_PROP_FOURCC, fourcc)
        # 设置最小人脸大小
        self.min_face_size = (int(self.photo_width * head_min_width),
                              int(self.photo_width * head_min_width))
        # 设置最大人脸大小
        self.max_face_size = (int(self.photo_width * head_max_width),
                              int(self.photo_width * head_max_width))
        # 读出的图像到预览图像缩放比例
        self.fx = self.preview_width / self.photo_width
        self.fy = self.preview_width / self.photo_width

    def __del__(self):
        self.cap.release()

    # 读取一帧视频, 找到人脸,并返回框选了人脸的预览小图象
    def get_preview_image(self):
        if not self.cap.isOpened():
            return False
        ret, self.frame = self.cap.read()
        if ret is True:
            # 水平翻转图像,存放在frame中
            self.frame = cv2.flip(self.frame, 1)
            # 灰度化,存放在gray中用于寻找人脸
            self.gray = cv2.cvtColor(self.frame, cv2.COLOR_BGR2GRAY)
            # 寻找人脸
            self.faces = self.face_model.detectMultiScale(
                self.gray, 1.1, 5, 0, self.min_face_size, self.max_face_size)
            # 转换为RGBA,存放在preview中,用于画人脸框,并缩小后显示在预览窗口中
            self.preview = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGBA)
            # 画出人脸
            for (x, y, w, h) in self.faces:
                cv2.rectangle(self.preview, (x, y), (x+w, y+h),
                              self.color, thickness=5)
                # 画出照片外框线,正确的用绿色,错误的用红色
                ret, x0, y0, x1, y1 = self.calc_photo_border(x, y, w, h)
                if ret == True:
                    cv2.rectangle(self.preview, (x0, y0), (x1, y1),
                                  self.color_ok, thickness=5)
                else:
                    cv2.rectangle(self.preview, (x0, y0), (x1, y1),
                                  self.color_err, thickness=5)
                    cv2.line(self.preview, (x0, y0), (x1, y1),
                             self.color_err, thickness=5)
                    cv2.line(self.preview, (x1, y0), (x0, y1),
                             self.color_err, thickness=5)
            # 缩放图像
            self.preview = cv2.resize(
                self.preview, None, fx=self.fx, fy=self.fy, interpolation=cv2.INTER_LINEAR)
            tmp_img = Image.fromarray(self.preview)
            self.img_tk = ImageTk.PhotoImage(image=tmp_img)
            return True
        else:
            return False

    # 保存当前照片
    def save_photo(self, filepath: str, filename: str):
        if filename is not None and len(filename) > 0:
            # 裁剪图像
            tmp_img = self.frame[self.y0:self.y1, self.x0:self.x1]
            # 锐度加强
            # kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]], np.float32) #定义一个核
            # tmp_img = cv2.filter2D(tmp_img, -1, kernel=kernel)
            # 统一尺寸
            tmp_img = cv2.resize(tmp_img, (self.final_photo_width,
                                 int(self.final_photo_width*self.h_w)), interpolation=cv2.INTER_CUBIC)
            # 删除tmp.jpg文件
            if os.path.exists(filepath + '/tmp.jpg'):
                os.remove(filepath + '/tmp.jpg')
            # 删除已保存的文件 
            if os.path.exists(filepath + '/' + filename):
                os.remove(filepath + '/' + filename)
            # 写入文件
            cv2.imwrite(filepath + '/tmp.jpg', tmp_img, [int(cv2.IMWRITE_JPEG_QUALITY), 100])
            # 重命名文件,因为cv2.imwrite不支持中文
            os.rename(filepath + '/tmp.jpg', filepath + '/' + filename)
        self.photo_ready = False
        pass

    def calc_photo_border(self, x: int, y: int, w: int, h: int):
        # 照片在大图像中高度
        photo_h = h * self.scale_h
        # 照片在大图像中宽度
        photo_w = photo_h / self.h_w
        # 脸中心坐标在大照片中x坐标
        face_center_x = x + w / 2
        # 脸中心坐标在大照片中y坐标
        face_center_y = y + h / 2
        # 左上角坐标
        x0 = int(face_center_x - photo_w / 2)
        y0 = int(face_center_y - photo_h * (1 - self.face_center_scale))
        # 右下角坐标
        x1 = int(face_center_x + photo_w / 2)
        y1 = int(face_center_y + photo_h * self.face_center_scale)

        # 判断坐标是否合法
        ret = True
        if x0 < 0:
            x0 = 0
            ret = False

        if y0 < 0:
            y0 = 0
            ret = False

        if x1 >= self.photo_width:
            x1 = self.photo_width
            ret = False

        if y1 >= self.photo_height:
            y1 = self.photo_height
            ret = False

        return ret, x0, y0, x1, y1

    # 尝试自动拍照, 并返回数据到canvas
    def take_photo(self, canvas: tk.Canvas, px: int, py: int):
        if not self.cap.isOpened():
            return False

        ret, self.frame = self.cap.read()
        if not ret:
            return False

        # 水平翻转图像,存放在frame中
        self.frame = cv2.flip(self.frame, 1)

        # 灰度化,存放在gray中用于寻找人脸
        self.gray = cv2.cvtColor(self.frame, cv2.COLOR_BGR2GRAY)

        # 寻找人脸
        self.faces = self.face_model.detectMultiScale(
            self.gray, 1.1, 5, 0, self.min_face_size, self.max_face_size)

        # 没有找到脸,返回失败
        if len(self.faces) == 0:
            return False

        # 查找鼠标点击的位置在哪一张脸上
        index = -1
        (x, y, w, h) = (0, 0, 0, 0)
        for i in range(len(self.faces)):
            (x, y, w, h) = self.faces[i]
            if px > x*self.fx and px < (x + w)*self.fx and py > y*self.fy and py < (y+h)*self.fy:
                index = i

        # 没有点中位置
        if index == -1:
            return False

        # 转换为RGBA,存放在preview中,用于画人脸框,并缩小后显示在预览窗口中
        self.preview = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGBA)

        ret, x0, y0, x1, y1 = self.calc_photo_border(x, y, w, h)

        # 保存裁剪框位置
        self.x0 = x0
        self.y0 = y0
        self.x1 = x1
        self.y1 = y1

        if ret == False:
            return False

        # 按比例减小尺寸
        self.preview = cv2.rectangle(
            self.preview, (x0, y0), (x1, y1), self.color, thickness=5)
        self.preview = cv2.resize(self.preview, None, fx=self.preview_width / self.photo_width,
                                  fy=self.preview_width / self.photo_width, interpolation=cv2.INTER_LINEAR)
        # 生成tk的格式
        tmp_img = Image.fromarray(self.preview)
        self.img_tk = ImageTk.PhotoImage(image=tmp_img)

        # 更新显示
        canvas.create_image(0, 0, anchor=tk.NW, image=self.img_tk)

        # 更新标志,返回True
        self.photo_ready = True
        return True


class DataServer():
    def __init__(self):
        self.imported = False
        self.cur_stu = 0
        self.pwd = os.getcwd()
        self.DCIM = self.pwd + '/DCIM'
        if os.path.exists(self.DCIM):
            pass
        else:
            os.makedirs(self.DCIM)

    def read_excel_file(self, file_path: str):
        self.filename = file_path
        if not os.path.exists(file_path):
            return False
        xl = xlrd.open_workbook(file_path)
        table = xl.sheets()[0]
        self.stu_arr = np.array([table.row_values(x)
                                 for x in range(1, table.nrows)])
        if len(self.stu_arr) > 0:
            self.imported = True
            self.stu_count = len(self.stu_arr)
            self.imported = True
            return True
        else:
            self.imported = False
            return False

    def generate_DCIM_path(self):
        return self.DCIM

    def generate_full_file_path(self, index: int):
        filename = self.generate_filename(index)
        if filename is not None:
            return self.DCIM + '/' + filename
        return None

    def generate_filename(self, index: int):
        if self.imported == False:
            return None

        if index == 0:
            if self.cur_stu == 0:
                index = 1
            else:
                index = self.cur_stu

        if index <= len(self.stu_arr) and index > 0:
            return '%05d-%s-%s-%s.jpg' % (int(float(self.stu_arr[index-1][0])), 
                                          (self.stu_arr[index-1][1]), 
                                          (self.stu_arr[index-1][2]), 
                                          (self.stu_arr[index-1][3]))

    def current_stu_info(self):
        if self.imported == False:
            return None

        if self.cur_stu == 0:
            self.cur_stu = 1

        return '%05d-%s-%s' % (int(float(self.stu_arr[self.cur_stu-1][0])), self.stu_arr[self.cur_stu-1][1], self.stu_arr[self.cur_stu-1][2])

    def set_current_stu(self, index: int):
        if self.imported == False:
            return False

        index = self.cur_stu + index
        if self.stu_count >= 1:
            if index <= 0:
                self.cur_stu = 1
            elif index > self.stu_count:
                self.cur_stu = self.stu_count
            else:
                self.cur_stu = index
            if os.path.exists(self.DCIM + '/' + self.generate_filename(self.cur_stu)):
                return True
            else:
                return False


class Application(tk.Frame):
    def __init__(self, cam: CameraControl):
        self.root = tk.Tk()
        self.data_server = DataServer()
        super().__init__(self.root)
        self.after_enable = False
        self.can_save_photo = False
        self.wnd_title = '望城区第二中学学生头像采集系统'
        self.msg = '请导入Excel文件'
        self.help = '使用方法:\n1,导入Excel文件,Excel文件第一列为序号,第二列为班级,第三列为姓名,第一行为标题. \n\n2, 通过向左,向右,向上,向下,Pgup, PgDn 选择从哪个学生开始照相 \n\n3, 鼠标左键点击人脸预拍照,图像静止后,再次左键点击保存照片,并自动切换到下一位学生,如果点击右键,则取消本次照相.\n\n4, 注意绿色的内框是人脸所在的位置,外框是照片裁剪框,如果外框是红色的,请调整位置,直至外框线为绿色\n\n'
        self.font1_name = '楷体'
        self.cam = cam
        self.root.resizable(0, 0)
        self.root.title(self.wnd_title)
        self.root.geometry('1088x489')
        self.place()
        self.init_wnd()

    # 左键用于启动拍照和确认照片
    def left_click(self, event: tk.Event):
        # 照相
        if self.can_save_photo == False:
            # 开始照相
            if self.after_enable == True:
                self.after_cancel(self.after_id)
                self.after_enable = False

            ret = self.cam.take_photo(self.Canvas_preview, event.x, event.y)

            # 没有成功识别到照片
            if ret == False:
                if self.after_enable == False:
                    self.after_id = self.after(int(1000/30), self.update_image)
                    self.after_enable = True
                self.can_save_photo = False
            else:
                self.can_save_photo = True
        # 保存
        else:
            self.can_save_photo = False
            filename = self.data_server.generate_filename(0)
            filepath = self.data_server.generate_DCIM_path()
            if filename is not None:
                self.cam.save_photo(filepath, filename)
                self.console_out('-----已拍照\n')
                self.data_server.set_current_stu(1)
                self.console_out(self.data_server.current_stu_info())
            else:
                self.console_out('未导入学生名册\n')

            if self.after_enable == False:
                self.after_id = self.after(int(1000/30), self.update_image)
                self.after_enable = True

    # 右键用于启动重拍
    def right_click(self, event: tk.Event):
        self.can_save_photo = False
        if self.after_enable == False:
            self.after_id = self.after(int(1000/30), self.update_image)
            self.after_enable = True
        pass

    def init_wnd(self):
        # font1 = tf.Font(family=self.font1_name, size=16)
        font1 = tf.Font(family=self.font1_name, size=14)

        self.Canvas_preview = tk.Canvas(
            self.root, width=640, height=486, bg='#000')
        self.Canvas_preview.place(x=0, y=0)
        self.Canvas_preview.bind("<Button-1>", self.left_click)
        self.Canvas_preview.bind("<Button-3>", self.right_click)

        self.Text_status = tk.Text(
            # self.root, fg='#0F0', bg='#000', font=font1, width=40, height=23, takefocus=0)
            self.root, fg='#0F0', bg='#000', font=font1, width=44, height=17, takefocus=0)
        self.Text_status.place(x=643, y=2)
        self.root.bind('<Key>', self.key_press)

        self.menu_bar = tk.Menu(self.root)

        self.import_menu = tk.Menu(self.menu_bar, tearoff=False)
        self.import_menu.add_command(
            label='选择Excel文件', command=self.select_excel_file)
        self.menu_bar.add_cascade(label="导入", menu=self.import_menu)
        self.root.config(menu=self.menu_bar)

        self.console_out(self.help)

    def select_excel_file(self):
        # filename = tk.filedialog.askopenfilename(title='选择Excel文件', filetypes=[
        #     ('Excel 2006', '*.xlsx'), ('Excel97-2003', '*.xls')])
        filename = tk.filedialog.askopenfilename(title='选择Excel文件', filetypes=[('Excel97-2003', '*.xls')])
        if self.data_server.read_excel_file(filename):
            self.console_out('\n打开文件:%s\n' % (filename))
            self.console_out('一共导入了%d个学生\n' % self.data_server.stu_count)
            self.console_out(self.data_server.current_stu_info())
        else:
            self.console_out('\n打开文件失败!\n')

    def update_image(self):
        self.cam.get_preview_image()
        self.Canvas_preview.create_image(
            0, 0, anchor=tk.NW, image=self.cam.img_tk)
        self.after_id = self.after(int(1000/30), self.update_image)
        self.after_enable = True

    def key_press(self, event: tk.Event):
        photoed = False
        valid_keypress = False

        if self.data_server.imported:
            if event.keycode == 38:
                photoed = self.data_server.set_current_stu(-1)
                valid_keypress = True
            elif event.keycode == 40:
                photoed = self.data_server.set_current_stu(1)
                valid_keypress = True
            elif event.keycode == 37:
                photoed = self.data_server.set_current_stu(-10)
                valid_keypress = True
            elif event.keycode == 39:
                photoed = self.data_server.set_current_stu(10)
                valid_keypress = True
            elif event.keycode == 33:
                photoed = self.data_server.set_current_stu(-50)
                valid_keypress = True
            elif event.keycode == 34:
                photoed = self.data_server.set_current_stu(50)
                valid_keypress = True

            if valid_keypress == True:
                if photoed:
                    self.console_out('\n%s-----已拍照' %
                                     self.data_server.current_stu_info())
                else:
                    self.console_out('\n%s' %
                                     self.data_server.current_stu_info())
        else:
            self.console_out('请先导入文件\n')

    def console_out(self, msg: str):
        self.Text_status.config(state=tk.NORMAL)
        self.Text_status.insert('end', msg)
        self.Text_status.yview(tk.MOVETO, 1.0)
        self.Text_status.config(state=tk.DISABLED)

    def start(self):
        self.root.mainloop()


if __name__ == '__main__':
    app = Application(CameraControl())
    app.update_image()
    app.start()
    sys.exit(0)
